﻿/*
 * This file is part of MonoStrategy.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://monostrategy.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;
using OpenTK;
using OpenTK.Input;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.InteropServices;
using System.Threading;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
    using OpenTK.Graphics.OpenGL;
#endif

namespace MonoStrategy.RenderSystem
{
    /// <summary>
    /// Seems a little odd to me that some hundred texture switches and GL.Begin/End clauses in conjunction
    /// with alpha blending are killing even a GTX 260, so here is the solution, a fast sprite engine based
    /// on alpha test instead of blending and using texture atlas as well as radix sort to schedule rendering
    /// tasks according to these atlas. Further, it supports three different modes of texture atlas 
    /// generation. The first one just generates them on the fly, which is rather expensive but fills the
    /// cache file with data for mode two, which will be used on next start then. Mode two uses usual bitmap
    /// images stored in the texture cache as atli, but still has to convert them into the driver internal format
    /// on the fly. The third mode needs manual processing by letting MonoStrategy generate texture atli for
    /// all animation library frames. These frames are stored as PNG files and can now be converted into DXT3
    /// compressed DDS files (NVidia Photoshop plugin or whatever) manually. Then another invokation of MonoStrategy
    /// will extend the image data in the texture cache with its DDS data. Next time the game loads, it will now
    /// directly use the compressed images within the texture cache, without any postprocessing done by the driver.
    /// Mode three is meant for production use, whereas the first two modes are the default setting for development,
    /// since they work fully automatic. We still have to keep the original image data in mode three, since some
    /// systems might not support DXT3 and thus need the uncompressed image data...
    /// </summary>
    [Serializable]
    public class SpriteEngine : IDisposable
    {
        [NonSerialized]
        private SortedDictionary<String, AnimationClass> m_NameToClass = new SortedDictionary<string, AnimationClass>();
        private readonly List<TextureAtlas> m_AtlasList = new List<TextureAtlas>();
        private readonly SortedDictionary<long, TextureAtlasEntry> m_ChecksumToEntry = new SortedDictionary<long, TextureAtlasEntry>();
        private readonly List<TextureAtlasEntry> m_FrameToEntry = new List<TextureAtlasEntry>();
        public List<long> CustomChecksums { get; private set; }
        [NonSerialized]
        private FileStream m_CacheFile;

        [OnDeserialized]
        private void OnDeserialized(StreamingContext ctx)
        {
            m_NameToClass = new SortedDictionary<string, AnimationClass>();
        }

        public int AtlasSize { get; private set; }

        [Serializable]
        private class TextureAtlasEntry
        {
            public RectangleDouble TexRect;
            public Rectangle PixRect;
            public long Checksum;
            public TextureAtlas Atlas;
            [NonSerialized]
            public Bitmap Image;
            public int AreaSize;
        }

        private struct TaskEntry
        {
            public Procedure<RectangleDouble> Handler;
            public RectangleDouble TexCoords;
        }

        [Serializable]
        private class TextureAtlas
        {
            [NonSerialized]
            private List<TaskEntry> m_Tasks = new List<TaskEntry>();
            public List<TaskEntry> Tasks { get { return m_Tasks; } }
            private Bitmap m_Image;
            [NonSerialized]
            private NativeTexture m_Texture;

            [OnDeserialized]
            private void OnDeserialized(StreamingContext ctx)
            {
                m_Tasks = new List<TaskEntry>();
            }

            public NativeTexture Texture
            {
                get
                {
                    if(m_Texture == null)
                        m_Texture = new NativeTexture(TextureOptions.None, m_Image);

                    return m_Texture;
                }
            }

            public TextureAtlas(AtlasTree inAtlasTree)
            {
                m_Image = new Bitmap(inAtlasTree.Rect.Width, inAtlasTree.Rect.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
                Graphics g = Graphics.FromImage(m_Image);

                using (g)
                {
                    ProcessNode(g, inAtlasTree);
                }

                m_Texture = new NativeTexture(TextureOptions.None, m_Image);
            }

            private void ProcessNode(Graphics g, AtlasTree node)
            {
                if (node == null)
                    return;

                if (node.Entry != null)
                {
                    g.DrawImageUnscaled(node.Entry.Image, node.Rect.Left, node.Rect.Top);

                    node.Entry.Atlas = this;
                    node.Entry.Image = null;
                }

                ProcessNode(g, node.Left);
                ProcessNode(g, node.Right);
            }
        }

        private class AtlasTree
        {
            public AtlasTree Left;
            public AtlasTree Right;
            public Rectangle Rect;
            public TextureAtlasEntry Entry;

            private AtlasTree()
            {
            }

            public AtlasTree(int inWidth, int inHeight)
            {
                Rect = new Rectangle(0, 0, inWidth, inHeight);
            }

            public AtlasTree Insert(TextureAtlasEntry inNewEntry)
            {

                if ((Left != null) || (Right != null))
                {
                    AtlasTree newNode = null;

                    if (Left != null)
                        newNode = Left.Insert(inNewEntry);

                    if (newNode != null)
                        return newNode;

                    return Right.Insert(inNewEntry);
                }
                else
                {
                    if (Entry != null)
                        return null;

                    if ((Rect.Width < inNewEntry.PixRect.Width) || (Rect.Height < inNewEntry.PixRect.Height))
                        return null;

                    if ((Rect.Width == inNewEntry.PixRect.Width) && (Rect.Height == inNewEntry.PixRect.Height))
                    {
                        Entry = inNewEntry;

                        return this;
                    }

                    Left = new AtlasTree();
                    Right = new AtlasTree();

                    int dw = Rect.Width - inNewEntry.PixRect.Width;
                    int dh = Rect.Height - inNewEntry.PixRect.Height;

                    if (dw > dh)
                    {
                        Left.Rect = new Rectangle(Rect.Left, Rect.Top, inNewEntry.PixRect.Width, Rect.Height);
                        Right.Rect = new Rectangle(Rect.Left + inNewEntry.PixRect.Width, Rect.Top, dw, Rect.Height);
                    }
                    else
                    {
                        Left.Rect = new Rectangle(Rect.Left, Rect.Top, Rect.Width, inNewEntry.PixRect.Height);
                        Right.Rect = new Rectangle(Rect.Left, Rect.Top + inNewEntry.PixRect.Height, Rect.Width, dh);
                    }

                    return Left.Insert(inNewEntry);
                }
            }
        }

        public void Dispose()
        {
            if (m_CacheFile != null)
                m_CacheFile.Dispose();

            m_CacheFile = null;
        }

        private SpriteEngine(FileStream inCacheFile, int inAtlasSize)
        {
            CustomChecksums = new List<long>();
            m_CacheFile = inCacheFile;
            AtlasSize = inAtlasSize;

            double log2Factor = 1.0 / Math.Log(2.0);
            int SizeShift = (int)Math.Floor((Math.Log((double)AtlasSize) * log2Factor) + 0.5);

            if ((int)Math.Pow(2, (double)SizeShift) != AtlasSize)
                throw new ArgumentException("Texture atlas width and height must be a power of two!");
        }

        public static SpriteEngine OpenOrCreate(String inCacheFile, Int32 inAtlasSize)
        {
            FileStream fileStream = new FileStream(inCacheFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
            SpriteEngine engine;

            if (fileStream.Length > 0)
            {
                // deserialize sprite engine
                BinaryFormatter format = new BinaryFormatter();

                try
                {
                    engine = (SpriteEngine)format.Deserialize(fileStream);
                    engine.m_CacheFile = fileStream;
                }
                catch
                {
                    // create a new one
                    engine = new SpriteEngine(fileStream, inAtlasSize);
                }
            }
            else
            {
                // create a new one
                engine = new SpriteEngine(fileStream, inAtlasSize);
            }

            return engine;
        }

        public void BeginUpdate()
        {
            m_AtlasList.Clear();
            m_ChecksumToEntry.Clear();
            m_FrameToEntry.Clear();
            m_NameToClass.Clear();
        }

        public void UpdateClass(AnimationClass inClass)
        {
            if (m_NameToClass.ContainsKey(inClass.Name))
                return;

            m_NameToClass.Add(inClass.Name, inClass);

            foreach (var set in inClass.Sets)
            {
                foreach (var anim in set.Animations)
                {
                    foreach (var frame in anim.Frames)
                    {
                        TextureAtlasEntry entry = new TextureAtlasEntry()
                        {
                             PixRect = new Rectangle(0, 0, frame.Width, frame.Height),
                             Image = frame.Source,
                             AreaSize = frame.Width * frame.Height,
                             Checksum = frame.Checksum,
                        };

                        if ((entry.PixRect.Width > AtlasSize) || (entry.PixRect.Height > AtlasSize))
                            throw new ArgumentOutOfRangeException("Frame size exceed texture atlas size!");

                        while (m_FrameToEntry.Count <= frame.Index)
                        {
                            m_FrameToEntry.Add(null);
                        }

                        if (m_ChecksumToEntry.ContainsKey(frame.Checksum))
                        {
                            // create reference to existing frame (currently this is already done implicitly by the animation library but may change in future)
                            m_FrameToEntry[frame.Index] = m_ChecksumToEntry[frame.Checksum];

                            continue;
                        }

                        m_ChecksumToEntry.Add(frame.Checksum, entry);
                        m_FrameToEntry[frame.Index] = entry;
                    }
                }
            }
        }

        public void EndUpdate()
        {
            AtlasTree m_AtlasTree = new AtlasTree(AtlasSize, AtlasSize);
            AtlasTree atlasNode;
            TextureAtlasEntry[] orderedEntries = m_ChecksumToEntry.Values.OrderByDescending(e => e.AreaSize).ToArray();
            double dAtlasSize = AtlasSize;

            // TODO: use a better algorithm for texture atlas generation, maybe genetic or swarm...
            while(orderedEntries.Any(e => e != null))
            {
                for(int i = 0; i < orderedEntries.Length; i++)
                {
                    var entry = orderedEntries[i];

                    if (entry == null)
                        continue;

                    if ((atlasNode = m_AtlasTree.Insert(entry)) == null)
                        continue;

                    entry.PixRect = atlasNode.Rect;
                    entry.TexRect = new RectangleDouble(
                        atlasNode.Rect.Left / dAtlasSize,
                        atlasNode.Rect.Top / dAtlasSize,
                        atlasNode.Rect.Width / dAtlasSize,
                        atlasNode.Rect.Height / dAtlasSize);

                    orderedEntries[i] = null;
                }

                m_AtlasList.Add(new TextureAtlas(m_AtlasTree));
                m_AtlasTree = new AtlasTree(AtlasSize, AtlasSize);
            }

            // save state to disk
            BinaryFormatter format = new BinaryFormatter();

            m_CacheFile.Position = 0;
            m_CacheFile.SetLength(0);

            format.Serialize(m_CacheFile, this);
            m_CacheFile.Flush();

            m_CacheFile.Position = 0;

            m_NameToClass.Clear();
        }

        public void BeginRadixRender()
        {
            foreach (var atlas in m_AtlasList)
            {
                atlas.Tasks.Clear();
            }
        }

        /// <summary>
        /// This is a little specialized, but since we are only dealing with frames in the main render pipeline,
        /// it is suiteable to have such a shortcut. The following will schedule the given task according to the
        /// atlas containing the given frame. When <see cref="EndRadixRender"/> is finally called, each atlas is
        /// only bound once to OpenGL and all tasks scheduled for this particular atlas are invoked. Further, we
        /// know that we are dealing with sprites here and thus only one GL.Begin/End clause is used per atlas,
        /// together resulting in the maximum performance one can archieve with sprites. The scheduling is performed
        /// in practical constant time, radix sort...
        /// 
        /// </summary>
        /// <param name="inFrame"></param>
        /// <param name="inTask">A callback, receiving the texture atlas subimage area in UV coordinates.</param>
        public void RadixRenderSchedule(int inFrameIndex, Procedure<RectangleDouble> inTask)
        {
            if (inTask == null)
                throw new ArgumentNullException();

            var frame = m_FrameToEntry[inFrameIndex];

            frame.Atlas.Tasks.Add(new TaskEntry()
            {
                Handler = inTask,
                TexCoords = frame.TexRect,
            });
        }


        public void EndRadixRender()
        {
            foreach (var atlas in m_AtlasList)
            {
                atlas.Texture.Bind();

                GL.Begin(BeginMode.Quads);
                {
                    foreach (var task in atlas.Tasks)
                    {
                        task.Handler(task.TexCoords);
                    }
                }
                GL.End();
            }
        }
    }
}
